/**
 * WebSocket消息解析器
 * 
 * @description
 * 负责处理WebSocket二进制消息的解析和封装：
 * - 解决TCP粘包问题，确保消息完整性
 * - 实现自定义消息协议的编码/解码
 * - 支持消息缓存和分片重组
 * - 提供消息长度校验和错误处理
 * 
 * 消息协议格式：
 * [消息头(4字节)] + [消息类型(4字节)] + [消息体(N字节)]
 * - 消息头：记录后续数据长度（消息号+消息体）
 * - 消息类型：标识消息类型
 * - 消息体：实际的业务数据
 * 
 * @author wz
 * @date 2024.9.10
 * @version 1.0.0
 */

import { SocketConst } from "./BaseTypes";

export class MsgParser {
    /**
     * 消息缓冲区
     * 用于缓存未完成的消息数据，解决TCP粘包和分包问题
     * 
     * @private
     */
    private _unit8Arr: Uint8Array = new Uint8Array(0);
    /**
     * 解析二进制消息数据
     * 从WebSocket接收的ArrayBuffer中解析出完整的消息，处理TCP粘包和分包问题
     * 
     * @param binary WebSocket接收到的二进制数据
     * @param handle 消息处理回调函数，参数为(消息ID, 消息体数据)
     * 
     * @description
     * 处理流程：
     * 1. 将新收到的数据追加到内部缓冲区
     * 2. 循环处理缓冲区数据，直到无法提取完整消息
     * 3. 每次提取一条完整消息后调用回调函数处理
     * 4. 更新缓冲区，移除已处理的数据
     * 
     * @example
     * ```typescript
     * parser.parse(event.data, (msgId, data) => {
     *   console.log(`收到消息: ID=${msgId}, 数据长度=${data.length}字节`);
     *   // 处理消息数据...
     * });
     * ```
     */
    parse(binary: ArrayBuffer, handle: (msgId: number, data: Uint8Array) => void) {
        // 将新收到的数据追加到缓冲区
        const data = new Uint8Array(binary);
        this._unit8Arr = this.concatUnit8Arrays(this._unit8Arr, data);

        // 循环处理所有完整消息
        while (this._unit8Arr.length >= SocketConst.MsgHeadSize) {
            // 1. 读取消息头，获取消息体长度
            const msgSize = this.readLengthFromBuffer(this._unit8Arr.slice(0, SocketConst.MsgHeadSize));

            // 2. 计算整个消息的完整长度(消息头 + 消息体)
            const completeSize = SocketConst.MsgHeadSize + msgSize;

            // 3. 检查是否有完整消息可供处理
            if (this._unit8Arr.length >= completeSize) {
                // 4. 提取消息类型ID
                const msgIdArr = this._unit8Arr.slice(SocketConst.MsgHeadSize, SocketConst.MsgHeadSize + SocketConst.MsgTypeSize);
                const msgId = this.readLengthFromBuffer(msgIdArr);

                // 5. 提取消息体数据
                const message = this._unit8Arr.slice(SocketConst.MsgHeadSize + SocketConst.MsgTypeSize, completeSize);

                // 6. 更新缓冲区，移除已处理的消息
                this._unit8Arr = this._unit8Arr.slice(completeSize);

                // 7. 调用回调函数处理消息
                if (handle) handle(msgId, message);
            } else {
                // 消息不完整，等待更多数据
                break;
            }
        }
    }    
    /**
     * 封装消息数据
     * 将消息ID和数据封装成符合协议格式的二进制数据，用于发送
     * 
     * @param msgId 消息ID，标识消息类型
     * @param data 要发送的消息体数据
     * @returns 封装后的二进制数据，可直接通过WebSocket发送
     * @throws 当消息大小超过限制时将返回undefined
     * 
     * @description
     * 封装过程：
     * 1. 计算消息总长度并进行大小检查
     * 2. 创建消息头，记录消息体长度
     * 3. 创建消息类型ID部分
     * 4. 拼接所有部分：消息头 + 消息类型 + 消息体
     */
    wrap(msgType: number, data: Uint8Array): Uint8Array {
        // 1. 计算消息体总长度 (消息类型ID + 实际数据)
        const msgSize = data.length + SocketConst.MsgTypeSize;

        // 2. 检查消息大小是否超过限制
        if (msgSize > SocketConst.MaxMsgSize) {
            console.error(`消息过大，无法发送：${msgSize}字节, 超过最大限制${SocketConst.MaxMsgSize}字节`);
            return;
        }

        // 3. 创建消息头部分，包含消息体长度信息
        const headBuffer = this.writeLengthToBuffer(msgSize);

        // 4. 创建消息类型Type部分
        const msgTypeBuffer = this.writeLengthToBuffer(msgType);

        // 5. 拼接所有部分：[消息头] + [消息类型ID] + [消息体数据]
        const wrapData = this.concatUnit8Arrays(headBuffer, msgTypeBuffer, data);
        return wrapData;
    }
    /**
     * 清空消息缓存
     * 重置内部缓冲区，通常在连接关闭或重置时调用
     * 
     * @description
     * 当连接断开或需要重置解析器状态时，应调用此方法清除所有未处理的缓存数据，
     * 避免旧数据影响新连接的消息解析。
     */
    clear() {
        this._unit8Arr = new Uint8Array(0);
        console.log("MsgParser: 消息缓冲区已清空");
    }

    /**
     * 从字节数组中读取32位整数值
     * 
     * @param arr 包含32位整数的字节数组
     * @returns 解析出的整数值
     * @throws 当数组长度不足4字节时可能会抛出异常
     * 
     * @description
     * 使用DataView从字节数组的起始位置读取一个32位整数。
     * 采用小端序(little-endian)表示，即低位字节在前。
     * 
     * @private 内部使用方法
     */
    private readLengthFromBuffer(arr: Uint8Array): number {
        return new DataView(arr.buffer).getInt32(0, true);
    }
    /**
     * 将32位整数写入字节数组
     * 
     * @param length 要写入的整数值
     * @returns 包含整数值的4字节数组
     * 
     * @description
     * 创建一个4字节的ArrayBuffer，并使用DataView将整数值写入。
     * 采用小端序(little-endian)表示，即低位字节在前。
     * 
     * @private 内部使用方法
     */
    private writeLengthToBuffer(length: number): Uint8Array {
        const view = new DataView(new ArrayBuffer(SocketConst.MsgTypeSize));
        view.setInt32(0, length, true);
        return new Uint8Array(view.buffer);
    }

    /**
     * 合并多个字节数组
     * 
     * @param params 要合并的字节数组列表
     * @returns 合并后的新字节数组
     * 
     * @description
     * 将多个Uint8Array合并成一个连续的新Uint8Array。
     * 实现步骤：
     * 1. 计算所有数组的总长度
     * 2. 创建足够大小的新数组
     * 3. 依次将每个源数组的内容复制到新数组中
     * 
     * @example
     * ```typescript
     * const arr1 = new Uint8Array([1, 2, 3]);
     * const arr2 = new Uint8Array([4, 5]);
     * const result = concatUnit8Arrays(arr1, arr2);
     * // result 将是 [1, 2, 3, 4, 5]
     * ```
     * 
     * @private 内部使用方法
     */
    private concatUnit8Arrays(...params: Uint8Array[]): Uint8Array {
        // 1. 计算合并后的总长度
        const totalLength = params.reduce((length: number, arr: Uint8Array) => {
            return length + arr.length;
        }, 0);

        // 2. 创建目标数组
        const result = new Uint8Array(totalLength);

        // 3. 复制所有源数组内容
        let offset = 0;
        for (let i = 0; i < params.length; i++) {
            result.set(params[i], offset);
            offset += params[i].length;
        }

        return result;
    }
}


